﻿
using EmperialApps.WeatherSpark.Data;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;

namespace EmperialApps.WeatherSpark.Service.Internal {

    /// <summary>Represents a source for forecasts.</summary>
    internal abstract class ForecastSource {

        /// <summary>Indicates the weather.gov source for US forecasts.</summary>
        public static readonly ForecastSource NOAA = new NoaaSource( );

        /// <summary>Indicates the yr.no source for global forecasts.</summary>
        public static readonly ForecastSource YRNO = new YrnoSource( );


        /// <summary>Gets the name of the container for the source.</summary>
        public string ContainerName { get { return _containerName; } }

        /// <summary>Determines whether there is a source URL available for the specified location.</summary>
        public abstract bool HasSourceUrl( Coordinate location, ForecastInfo info );

        /// <summary>Gets the source URL for the specified location.</summary>
        public abstract string GetSourceUrl( Coordinate location, ForecastInfo info );

        /// <summary>Gets the source URL for the specified location.</summary>
        public abstract string GetSourceUrl( Coordinate location, Func<Coordinate, ForecastInfo> getInfo );

        /// <summary>Gets the name of a forecast from its description.</summary>
        public static string GetForecastName( string description ) {
            int sourceIndex = description.LastIndexOf( '|' );
            return sourceIndex < 0
                 ? description
                 : description.Substring( 0, sourceIndex );
        }

        /// <summary>Tries to create a new forecast info with the specified source URL, if it is for a recognized source.</summary>
        public static bool SaveForecastSourceUrl( Coordinate location, string sourceUrl, out ForecastInfo info ) {
            info = NOAA.SaveValidSourceUrl( location, sourceUrl )
                ?? YRNO.SaveValidSourceUrl( location, sourceUrl );

            return info != null;
        }

        /// <summary>Updates the info for a successful forecast download.</summary>
        public virtual void Update( ForecastInfo info, Forecast forecast ) {
            info.Description = GetForecastName( forecast.Description );
        }

        /// <summary>Updates the info for a failed forecast download.</summary>
        public virtual void Update( ForecastInfo info, ref bool persistentError ) { }

        /// <summary>Sets a redirect for the old coordinate location to the new info location.</summary>
        public void Redirect( ForecastInfoTableStore forecastInfo, ForecastInfo info, Coordinate oldLocation ) {
            var redirect = forecastInfo.Get( oldLocation );
            if( redirect == null ) {
                redirect = new ForecastInfo( oldLocation );
                InitializeRedirect( redirect, info );
                forecastInfo.Add( redirect );
            }
            else {
                InitializeRedirect( redirect, info );
                forecastInfo.Update( redirect );
            }
        }

        /// <summary>Determines whether the source can be used as a fallback for the specified forecast.</summary>
        public virtual void TryFallback( ForecastInfo info, object state, TryFallbackComplete callback ) {
            callback( info, state, null );
        }

        /// <summary>The callback delegate used by the source to indicate when it has determined whether a fallback for the specified forecast is possible.</summary>
        public delegate void TryFallbackComplete( ForecastInfo info, object state, Action<ForecastInfo> initializeFallback );

        /// <inheritdoc/>
        public sealed override string ToString( ) {
            return this._sourceName;
        }


        #region Private Members

        private readonly string _sourceName;
        private readonly string _containerName;

        private ForecastSource( string sourceName ) {
            this._sourceName = sourceName;
            this._containerName = sourceName.ToLowerInvariant( );
        }


        protected abstract ForecastInfo SaveValidSourceUrl( Coordinate location, string sourceUrl );

        protected virtual void InitializeRedirect( ForecastInfo redirect, ForecastInfo info ) {
            redirect.ForecastUrl = info.ForecastUrl;

            if( info.Description != null )
                redirect.Description = '\u2192' + info.Description;
        }


        private sealed class NoaaSource : ForecastSource {
#if DEBUG
            private const int FailureStatusLimit = 3;
#else
            private const int FailureStatusLimit = 32;
#endif
            private const int RedirectStatusLimit = 1 + FailureStatusLimit;
            private static int _uniquifier;

            public NoaaSource( ) : base( "NOAA" ) { }

            public override bool HasSourceUrl( Coordinate location, ForecastInfo info ) {
                return HasSourceUrl( info );
            }

            public override string GetSourceUrl( Coordinate location, ForecastInfo info ) {
                return GetSourceUrl( location );
            }

            public override string GetSourceUrl( Coordinate location, Func<Coordinate, ForecastInfo> getInfo ) {
                return GetSourceUrl( location );
            }

            protected override ForecastInfo SaveValidSourceUrl( Coordinate location, string sourceUrl ) {
                return null; // URL always generated based on location
            }

            protected override void InitializeRedirect( ForecastInfo redirect, ForecastInfo info ) {
                base.InitializeRedirect( redirect, info );

                redirect.NoaaDownloadStatus = RedirectStatusLimit;
            }

            public override void Update( ForecastInfo info, Forecast forecast ) {
                base.Update( info, forecast );

                // If download succeeded, reset status back to success.
                if( info.NoaaDownloadStatus != 0 && !forecast.HasRedirect( ).GetValueOrDefault( ) ) {
                    TraceEventType.Information.Trace( "Restoring {0} status on {1} from forecast {2}", this, info, forecast );
                    info.NoaaDownloadStatus = 0;
                }
            }

            public override void Update( ForecastInfo info, ref bool persistentError ) {
                // If error is persistent, move to limit; otherwise, update error count.
                info.NoaaDownloadStatus =
                    persistentError
                        ? FailureStatusLimit
                        : Math.Min( FailureStatusLimit, info.NoaaDownloadStatus + 1 );

                persistentError = !HasSourceUrl( info );
            }

            private static bool HasSourceUrl( ForecastInfo info ) {
                // Only allow download if we are not experiencing persistent failures.
                return info.NoaaDownloadStatus < FailureStatusLimit;
            }

            private static string GetSourceUrl( Coordinate location ) {
                uint uniquifier = unchecked( (uint)Interlocked.Increment( ref _uniquifier ) % 100 );
                return string.Format(
                    "http://forecast.weather.gov/MapClick.php?lat={0:0.00}00{2:00}&lon={1:0.00}00{2:00}&FcstType=digitalDWML",
                    location.Latitude, location.Longitude, uniquifier );
            }
        }

        private sealed class YrnoSource : ForecastSource {
            public YrnoSource( ) : base( "YRNO" ) { }

            public override bool HasSourceUrl( Coordinate location, ForecastInfo info ) {
                return IsValidSourceUrl( info.YrnoSourceUrl );
            }

            public override string GetSourceUrl( Coordinate location, ForecastInfo info ) {
                return info.YrnoSourceUrl;
            }

            public override string GetSourceUrl( Coordinate location, Func<Coordinate, ForecastInfo> getInfo ) {
                ForecastInfo info = getInfo( location );
                return this.GetSourceUrl( location, info );
            }

            protected override ForecastInfo SaveValidSourceUrl( Coordinate location, string sourceUrl ) {
                bool isYrnoSourceUrl =
                       sourceUrl != null
                    && sourceUrl.StartsWith( Place.YrnoAddress )
                    && sourceUrl.EndsWith( "/" );

                return isYrnoSourceUrl
                     ? new ForecastInfo( location ) { YrnoSourceUrl = Path.Combine( sourceUrl, "forecast.xml" ) }
                     : null;
            }

            protected override void InitializeRedirect( ForecastInfo redirect, ForecastInfo info ) {
                base.InitializeRedirect( redirect, info );

                if( info.YrnoSourceUrl == null )
                    info.YrnoSourceUrl = redirect.YrnoSourceUrl;
                redirect.YrnoSourceUrl = "";
            }

            public override void TryFallback( ForecastInfo info, object state, TryFallbackComplete callback ) {
                // If we already have a URL, indicate whether it is valid.
                if( !string.IsNullOrEmpty( info.YrnoSourceUrl ) ) {
                    bool valid = IsValidSourceUrl( info.YrnoSourceUrl );
                    TraceEventType.Verbose.Trace( "{0} already {1} fallback URL for {2}", this, valid ? "found" : "failed", info );

                    var initialize = valid ? new Action<ForecastInfo>( delegate { } ) : null;
                    callback( info, state, initialize );
                }
                // Otherwise, begin search for nearest available location.
                else {
                    TraceEventType.Verbose.Trace( "Searching for {0} fallback URL for {1}", this, info );
                    Coordinate location = info.GetLocation( );
                    Uri address = new Uri( string.Format( @"http://api.geonames.org/findNearbyPlaceName?username=weatherspark&style=FULL&lat={0}&lng={1}", location.Latitude, location.Longitude ) );
                    var userToken = Tuple.Create( info, state, callback );

                    var client = new WebClient( );
                    client.OpenReadCompleted += OnFallbackSearchComplete;
                    client.OpenReadAsync( address, userToken );
                }
            }

            private static bool IsValidSourceUrl( string yrnoSourceUrl ) {
                return !string.IsNullOrEmpty( yrnoSourceUrl )
                    && yrnoSourceUrl[0] != '-';
            }

            private static void OnFallbackSearchComplete( object sender, OpenReadCompletedEventArgs e ) {
                var userToken = (Tuple<ForecastInfo, object, TryFallbackComplete>)e.UserState;
                ForecastInfo info = userToken.Item1;
                object state = userToken.Item2;
                TryFallbackComplete callback = userToken.Item3;

                Place[] places;
                try {
                    using( (WebClient)sender )
                    using( e.Result )
                        places = Place.Load( Units.Metric, e.Result, default( Place.Corrections ) );
                }
                catch( Exception ex ) {
                    ex.Report( "YRNO Fallback Search Failure" );
                    places = new Place[0];
                }

                Action<ForecastInfo> initialize;
                Place place = places.ElementAtOrDefault( 0 );
                ForecastInfo validated = YRNO.SaveValidSourceUrl( place.Location, place.Link );
                if( validated != null ) {
                    string yrnoSourceUrl = validated.YrnoSourceUrl;
                    TraceEventType.Verbose.Trace( "Found YRNO fallback: " + yrnoSourceUrl );
                    initialize = i => i.YrnoSourceUrl = yrnoSourceUrl;
                }
                else {
                    TraceEventType.Warning.Trace( "YRNO fallback search failed for " + info );
                    initialize = null;
                }

                callback( info, state, initialize );
            }
        }

        #endregion

    }

}
